/**
 * JavaScript utilities that make life easier.
 */
function PZUtil() {
	/**
	 * Formats a date via a specific mask.
	 * 
	 * d:		Day of the month as digits; no leading zero for single-digit days.
	 * dd:		Day of the month as digits; leading zero for single-digit days.
	 * ddd:		Day of the week as a three-letter abbreviation.
	 * dddd:	Day of the week as its full name.
	 * m:		Month as digits; no leading zero for single-digit months.
	 * mm:		Month as digits; leading zero for single-digit months.
	 * mmm:		Month as a three-letter abbreviation.
	 * mmmm:	Month as its full name.
	 * yy:		Year as last two digits; leading zero for years less than 10.
	 * yyyy:	Year represented by four digits.
	 * 
	 * h:		hours; no leading zero for single-digit hours (12-hour clock).
	 * hh:		hours; leading zero for single-digit hours (12-hour clock).
	 * H:		hours; no leading zero for single-digit hours (24-hour clock).
	 * HH:		hours; leading zero for single-digit hours (24-hour clock).
	 * n: 		minutes; no leading zero for single-digit minutes.
	 * nn:		minutes; a leading zero for single-digit minutes.
	 * s:		seconds; no leading zero for single-digit seconds.
	 * ss:		seconds; leading zero for single-digit seconds.
	 * t:		one-character time marker string, such as A or P.
	 * tt:		multiple-character time marker string, such as AM or PM.
	 * 
	 * @param {Date} date - base date.
	 * @param {String} mask - given format.
	 * 
	 * @return {String}
	 */
	this.dateFormat = function(date, mask) {
		if (mask == null) mask = "mm/dd/yyyy";
		var monthNames = new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December");
		var weekdayNames = new Array("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday");
		var month, day, year, weekday, hour, minute, second, ap;
		
		if (date instanceof Date && !isNaN(date)) {
			hour = date.getHours();
			minute = date.getMinutes();
			second = date.getSeconds();
			if (hour >= 12) ap = "PM";
			else ap = "AM";
			
			mask = mask.replace(/HH/g, hour.toString().length == 1 ? "0" + hour : hour);
			mask = mask.replace(/H/g, hour);
			if (hour > 12) { hour -= 12; }
			mask = mask.replace(/hh/g, hour.toString().length == 1 ? "0" + hour : hour);
			mask = mask.replace(/h/g, hour);
			mask = mask.replace(/nn/g, minute.toString().length == 1 ? "0" + minute : minute);
			mask = mask.replace(/n/g, minute);
			mask = mask.replace(/ss/g, second.toString().length == 1 ? "0" + second : second);
			mask = mask.replace(/s/g, second);
			mask = mask.replace(/tt/g, "AP_L");
			mask = mask.replace(/t/g, "AP_S");
			
			month = date.getMonth();
			day = date.getDate();
			year = date.getFullYear();
			weekday = date.getDay();
			
			mask = mask.replace(/mmmm/g, "M_L");
			mask = mask.replace(/mmm/g, "M_S");
			mask = mask.replace(/mm/g, (month + 1).toString().length == 1 ? "0" + (month + 1) : month + 1);
			mask = mask.replace(/m/g, month + 1);
			mask = mask.replace(/dddd/g, "W_L");
			mask = mask.replace(/ddd/g, "W_S");
			mask = mask.replace(/dd/g, day.toString().length == 1 ? "0" + day : day);
			mask = mask.replace(/d/g, day);
			mask = mask.replace(/yyyy/g, year);
			mask = mask.replace(/yy/g, year.toString().substr(2, 2));
			
			mask = mask.replace(/M_L/g, monthNames[month]);
			mask = mask.replace(/M_S/g, monthNames[month].substr(0, 3));
			mask = mask.replace(/W_L/g, weekdayNames[weekday]);
			mask = mask.replace(/W_S/g, weekdayNames[weekday].substr(0, 3));
			mask = mask.replace(/AP_L/g, ap);
			mask = mask.replace(/AP_S/g, ap.substr(0, 1));
			
			return mask;
		}
		else
			throw SyntaxError("Invalid [Date] object.");
	};
	
	/**
	 * Determines the integer number of units by which date1 is less than date2
	 * 
	 * s:	Precise to the second (default)
	 * n:	Precise to the minute
	 * h:	Precise to the hour
	 * d:	Precise to the day
	 * w:	Precise to the week
	 * m:	Precise to the month
	 * y:	Precise to the year
	 * 
	 * @param {Date} date1
	 * @param {Date} date2
	 * @param {String} datePart - Precision of the comparison.
	 * @return {Number} A number of units, of type datePart.
	 */
	this.dateDiff = function(date1, date2, datePart) {
		var timeDiff = date2 - date1;
		var second = 1000, minute = second * 60, hour = minute * 60, day = hour * 24, week = day * 7;
		
		if (datePart == "s")
			return Math.floor(timeDiff / second);
		else if (datePart == "n")
			return Math.floor(timeDiff / minute);
		else if (datePart == "h")
			return Math.floor(timeDiff / hour); 
		else if (datePart == "d")
			return Math.floor(timeDiff / day); 
		else if (datePart == "w")
			return Math.floor(timeDiff / week);
		else if (datePart == "m")
			return (date2.getFullYear() * 12 + date2.getMonth()) - (date1.getFullYear() * 12 + date1.getMonth());
		else if (datePart == "y")
			return date2.getFullYear() - date1.getFullYear();
	};
	
	/**
	 * Performs a full date/time comparison of two dates.
	 * 
	 * s:	Precise to the second (default)
	 * n:	Precise to the minute
	 * h:	Precise to the hour
	 * d:	Precise to the day
	 * m:	Precise to the month
	 * y:	Precise to the year
	 * 
	 * @param {Date} date1
	 * @param {Date} date2
	 * @param {String} datePart - Precision of the comparison.
	 * @return {Number} 0 if equal, -1 if date2 is later, 1 if date1 is later.
	 */
	this.dateCompare = function(date1, date2, datePart) {
		if (datePart == null) datePart = "s";
		
		if (date1.getFullYear() > date2.getFullYear())
			return 1;
		else if (date1.getFullYear() < date2.getFullYear())
			return -1;
		else {
			if (datePart == "y")
				return 0;
			else {
				if (date1.getMonth() > date2.getMonth())
					return 1;
				else if (date1.getMonth() < date2.getMonth())
					return -1;
				else {
					if (datePart == "m")
						return 0;
					else {
						if (date1.getDate() > date2.getDate())
							return 1;
						else if (date1.getDate() < date2.getDate())
							return -1;
						else {
							if (datePart == "d")
								return 0;
							else {
								if (date1.getHours() > date2.getHours())
									return 1;
								else if (date1.getHours() < date2.getHours())
									return -1;
								else {
									if (datePart == "h")
										return 0;
									else {
										if (date1.getMinutes() > date2.getMinutes())
											return 1;
										else if (date1.getMinutes() < date2.getMinutes())
											return -1;
										else {
											if (datePart == "n")
												return 0;
											else {
												if (date1.getSeconds() > date2.getSeconds())
													return 1;
												else if (date1.getSeconds() < date2.getSeconds())
													return -1;
												else
													return 0;
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}
	};
	
	/**
	 * Gets how many days there are in a given month.
	 * 
	 * @param {Number} month - given month, starts with 0.
	 * @param {Number} year - given 4 digit year.
	 * 
	 * @return {Number}
	 */
	this.daysInMonth = function(month, year) {
		var d = new Date(year, month, 0);
		
		return d.getDate();
	};
	
	/**
	 * Adds units of time to a date.
	 * 
	 * datePart - String:
	 * d: day
	 * m: month
	 * y: year
	 * h: hour
	 * n: minute
	 * s: second
	 * 
	 * @param {String} datePart - date part.
	 * @param {Number} number - number of units of datePart to add to date.
	 * @param {Date} date - base date.
	 * 
	 * @return {Date}
	 */
	this.dateAdd = function(datePart, number, date) {
		var tmpDate = new Date(date);
		switch(datePart) {
			case "d":
				tmpDate.setDate(tmpDate.getDate() + number);
				break;
			case "m":
				tmpDate.setMonth(tmpDate.getMonth() + number);
				break;
			case "y":
				tmpDate.setYear(tmpDate.getFullYear() + number);
				break;
			case "h":
				tmpDate.setHours(tmpDate.getHours() + number);
				break;
			case "n":
				tmpDate.setMinutes(tmpDate.getMinutes() + number);
				break;
			case "s":
				tmpDate.setSeconds(tmpDate.getSeconds() + number);
				break;
		}
		
		return tmpDate;
	};
	
	/**
	 * Determines if given object is an integer.
	 * 
	 * @param {Object} x - the object to test.
	 * 
	 * @return {Boolean}
	 */
	this.isInt = function(x) {
		var filter = /^-?\d+$/;
		
		if (x.toString().match(filter) == null)
			return false;
		
		return true;
	};
	
	/**
	 * Determines if given string is in a valid URL format.
	 * 
	 * @param {String} str - string to test.
	 * 
	 * @returns {Boolean}
	 */
	this.isUrl = function(str) {
		var re = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
		
		return re.test(str);
	};
	
	/**
	 * Determines if given string is in a valid E-mail address format.
	 * 
	 * @param {String} str - string to test.
	 * 
	 * @return {Boolean}
	 */
	this.isEmail = function(str) {
		var filter = /^\S+@\S+\.[a-zA-Z0-9]{1,5}$/;
		
		if (str.match(filter) == null)
			return false;
		
		return true;
	};
	
	/**
	 * Determines if given object is a Date object.
	 * 
	 * @param {Object} strDate - given object to test.
	 * 
	 * @return {Boolean}
	 */
	this.isDate = function(strDate) {
		var dateResult = new Date(strDate);
		
		return (!isNaN(dateResult));
	};
	
	/**
	 * Validates the given two date range inputs. 
	 * 
	 * @param {Object} startDate - jQuery selected element of "start" date input.
	 * @param {Object} endDate - jQuery selected element of "end" date input.
	 */
	this.validateDateRange = function(startDate, endDate) {
		startDate.unbind("change").change(function() {
			if (endDate.val() == "") endDate.val(startDate.val());
			else {
				var start = new Date(startDate.val());
				var end = new Date(endDate.val());
				
				if (end < start) endDate.val(startDate.val());
			}
		});
		
		endDate.unbind("change").change(function() {
			if (startDate.val() == "") startDate.val(endDate.val());
			else {
				var start = new Date(startDate.val());
				var end = new Date(endDate.val());
				
				if (end < start) startDate.val(endDate.val());
			}
		});
	};
}

/**
 * Creates and manipulates a Queue data structure.
 */
function PZQueue() {
	var data = [];
	
	/**
	 * Inserts the specified element into this queue, if possible.
	 * 
	 * @param obj - the element to insert.
	 * @returns true if it was possible to add the element to this queue, else false.
	 */
	this.enqueue = function(obj) {
		data.push(obj);
	};
	
	/**
	 * Retrieves, but does not remove, the head of this queue, returning null if this queue is empty.
	 * 
	 * @returns the head of this queue, or null if this queue is empty.
	 */
	this.peek = function() {
		if (this.isEmpty())
			return null;
		
		return data[0];
	};
	
	/**
	 * Retrieves and removes the head of this queue, or null if this queue is empty.
	 * 
	 * @returns the head of this queue, or null if this queue is empty.
	 */
	this.dequeue = function() {
		if (this.isEmpty())
			return null;
		
		return data.shift();
	};
	
	/**
	 * Checks if the specified element exists in this queue.
	 * 
	 * @param obj - the element to check.
	 * @returns true if element exists, else false.
	 */
	this.exists = function(obj) {
		for (var i = 0; i < data.length; i++) {
			if (data[i] == obj)
				return true;
		}
		
		return false;
	};
	
	/**
	 * Checks to see if this queue is empty.
	 * 
	 * @returns true if this queue is empty, else false.
	 */
	this.isEmpty = function() {
		return data.length == 0;
	};
	
	/**
	 * Gets number of nodes in this queue.
	 * 
	 * @returns Integer value of number of nodes in this queue.
	 */
	this.getLength = function() {
		return data.length;
	};
	
	/**
	 * Sorts this queue with given function.
	 * 
	 * @param sortf - function to sort.
	 */
	this.sort = function(sortf) {
		if (sortf == null)
			data.sort();
		else
			data.sort(sortf);
	};
}

/**
 * Creates and manipulates a Stack data structure.
 */
function PZStack() {
	var data = [];
	
	/**
	 * tests if this stack is empty.
	 * 
	 * @returns true if this stack is empty, else false.
	 */
	this.isEmpty = function() {
		return data.length == 0;
	};
	
	/**
	 * looks at the object at the top of this stack without removing it from the stack.
	 * 
	 * @returns the object at the top of this stack.
	 */
	this.peek = function() {
		if (this.isEmpty())
			return null;
		
		return data[0];
	};
	
	/**
	 * removes the object at the top of this stack and returns that object as the value of this function.
	 * 
	 * @returns the object at the top of this stack, or null if this queue is empty
	 */
	this.pop = function() {
		if (this.isEmpty())
			return null;
		
		return data.pop();
	};
	
	/**
	 * pushes an item onto the top of this stack.
	 * 
	 * @param obj - the item to be pushed onto this stack.
	 * @returns the obj argument.
	 */
	this.push = function(obj) {
		data.push(obj);
		
		return obj;
	};
	
	/**
	 * returns the 1-based position where an object is on this stack.
	 * 
	 * @param obj - the desired object.
	 * @returns the 1-based position from the top of the stack where the object is located; the return value -1 indicates that the object is not on the stack.
	 */
	this.search = function(obj) {
		for (var i = 0; i < data.length; i++) {
			if (data[i] == obj)
				return i + 1;
		}
		
		return -1;
	};
}

/**
 * Creates nice-looking alert boxes.
 */
function PZAlert() {
	var alertDialogName = "pzAlertDialog";
	var alertDialog = null;
	var loadingDialogName = "pzLoadingDialog";
	var loadingDialog = null;
	var loadingIcon = "<div class='pzLoadingIcon'></div>";
	
	this.getLoadingIcon = function() {
		return loadingIcon;
	};
	
	this.showAlert = function(titleText, message, url) {
		alertDialog.html(message).dialog({
			title: titleText,
			buttons: {
				"Ok": function() {
					if (url == null)
						$(this).dialog("close");
					else
						window.location.href = url;
				}
			}
		}).dialog("open");
	};
	
	this.showError = function(message, url) {
		message = "<div style='color: #c30;'>" + message + "</div>";
		this.showAlert("Error", message, url);
	};
	
	this.pageLoading = function() {
		loadingDialog.dialog("open");
	};
	
	this.closePageLoading = function() {
		loadingDialog.dialog("close");
	};
	
	var init = function() {
		if ($("#" + alertDialogName).length == 0) {
			$("body").append("<div id='" + alertDialogName + "'></div>");
		}
		
		if ($("#" + loadingDialogName).length == 0) {
			$("body").append("<div id='" + loadingDialogName + "'></div>");
		}
		
		alertDialog = $("#" + alertDialogName);
		loadingDialog = $("#" + loadingDialogName);
		
		alertDialog.dialog({
			autoOpen: false,
			modal: true,
			resizable: false,
			width: 300,
			height: 150
		});
		
		loadingDialog.html(loadingIcon).dialog({
			autoOpen: false,
			modal: true,
			resizable: false,
			height: 100,
			width: 200,
			title: "Loading..."
		});
	};
	
	$(document).ready(function() {
		init();
	});
}